/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.persistence.basis.criteria;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.genia.toolbox.basics.bean.Pair;
import com.genia.toolbox.basics.bean.impl.PairImpl;
import com.genia.toolbox.persistence.criteria.Criteria;
import com.genia.toolbox.persistence.criteria.CriteriaType;
import com.genia.toolbox.persistence.criteria.PropertyField;
import com.genia.toolbox.persistence.criteria.criterion.Criterion;
import com.genia.toolbox.persistence.criteria.criterion.Junction;
import com.genia.toolbox.persistence.criteria.field.Field;
import com.genia.toolbox.persistence.criteria.order.Order;
import com.genia.toolbox.persistence.manager.PersistenceManager;

/**
 * the basis for the {@link Criteria} implementation.
 * 
 * @param <MAINTYPE>
 *          the main type of objects to retrieve
 */
public abstract class AbstractCriteria<MAINTYPE>
    implements Criteria<MAINTYPE>
{

  /**
   * Perfrom an inner join of this criteria with the elements described by the
   * {@link PropertyField} passed in parameter.
   * 
   * @param <TYPE>
   *          the type of the object with which we do a join
   * @param <INNERTYPE>
   *          the type of the elements of the property
   * @param criteriaType
   *          the {@link CriteriaType} describing the object that will be
   *          inspected
   * @param propertyField
   *          the description of the field of the object on which a join will be
   *          done
   * @return the {@link CriteriaType} to be used to refer to the new object in
   *         the join
   * @see com.genia.toolbox.persistence.criteria.Criteria#join(com.genia.toolbox.persistence.criteria.CriteriaType,
   *      com.genia.toolbox.persistence.criteria.PropertyField)
   */
  public <TYPE, INNERTYPE> CriteriaType<INNERTYPE> join(CriteriaType<?> criteriaType, PropertyField<TYPE, INNERTYPE> propertyField)
  {
    final CriteriaType<INNERTYPE> res = newCriteriaType(propertyField.getElementsClass());
    final PairImpl<CriteriaType<?>, PropertyField<?, ?>> pair = new PairImpl<CriteriaType<?>, PropertyField<?, ?>>();
    pair.setFirstElement(criteriaType);
    pair.setSecondElement(propertyField);
    innerJoins.put(res, pair);
    addProjection(res);
    return res;
  }



  /**
   * Perfrom an inner join of this criteria with the elements described by the
   * {@link PropertyField} passed in parameter.
   * 
   * @param <TYPE>
   *          the type of the object with which we do a join
   * @param <INNERTYPE>
   *          the type of the elements of the property
   * @param propertyField
   *          the description of the field of the main object on which a join
   *          will be done
   * @return the {@link CriteriaType} to be used to refer to the new object in
   *         the join
   * @see com.genia.toolbox.persistence.criteria.Criteria#join(com.genia.toolbox.persistence.criteria.PropertyField)
   */
  public <TYPE, INNERTYPE> CriteriaType<INNERTYPE> join(PropertyField<TYPE, INNERTYPE> propertyField)
  {
    return join(getMainType(), propertyField);
  }

  /**
   * the list of {@link Order} associated to this query.
   */
  private final transient List<Order> orders = new ArrayList<Order>();

  /**
   * the list of {@link Field} to group by.
   */
  private final transient List<Field> groupBys = new ArrayList<Field>();

  /**
   * the list of {@link CriteriaType} representing the base query.
   */
  private final transient List<CriteriaType<?>> baseQuery = new ArrayList<CriteriaType<?>>();

  /**
   * the inner joins associated to this query. Each inner join is the
   * association of a {@link CriteriaType} to the element allowing to determine
   * which field must be added.
   */
  private final transient Map<CriteriaType<?>, Pair<? extends CriteriaType<?>, PropertyField<?, ?>>> innerJoins = new LinkedHashMap<CriteriaType<?>, Pair<? extends CriteriaType<?>, PropertyField<?, ?>>>();

  /**
   * the {@link CriteriaType} of the type of the main type of this query.
   */
  private final transient CriteriaType<MAINTYPE> mainCriteriaType;

  /**
   * the {@link CriteriaType} of the type of the original type of this query.
   */
  private final transient CriteriaType<?> originalCriteriaType;

  /**
   * the projections associated to this query. Each projection is the
   * association of a {@link CriteriaType} to a {@link Field} representing the
   * projection to be done.
   */
  private transient Map<CriteriaType<?>, Field> projections = null;

  /**
   * the {@link PersistenceManager} associated to the current implementation.
   */
  private final transient PersistenceManager persistenceManager;

  /**
   * the restrictions associated to thge current query.
   */
  private transient Junction restrictions = null;

  /**
   * wether the query must return distinct result or not.
   */
  private boolean distinct = false;



  /**
   * Constructor that allow to create projections.
   * 
   * @param modele
   *          the original {@link Criteria}
   * @param mainCriteriaType
   *          the {@link CriteriaType} of the main element of the projection.
   * @param field
   *          the {@link Field} representing the main projection
   */
  protected AbstractCriteria(final AbstractCriteria<?> modele, final CriteriaType<MAINTYPE> mainCriteriaType, final Field field)
  {
    this.orders.addAll(modele.orders);
    this.baseQuery.addAll(modele.baseQuery);
    this.innerJoins.putAll(modele.innerJoins);
    this.groupBys.addAll(modele.groupBys);
    this.persistenceManager = modele.persistenceManager;
    this.projections = new HashMap<CriteriaType<?>, Field>();
    this.originalCriteriaType = modele.originalCriteriaType;
    this.distinct = modele.distinct;
    this.restrictions = modele.restrictions == null ? null : (Junction) modele.restrictions.clone();
    this.mainCriteriaType = mainCriteriaType;
    projections.put(mainCriteriaType, field);
  }



  /**
   * Constructor.
   * 
   * @param persistenceManager
   *          the {@link PersistenceManager} associated to the current
   *          implementation
   * @param mainType
   *          the class of the main type of this query
   */
  protected AbstractCriteria(final PersistenceManager persistenceManager, final Class<MAINTYPE> mainType)
  {
    this.persistenceManager = persistenceManager;
    mainCriteriaType = newCriteriaType(mainType);
    originalCriteriaType = mainCriteriaType;
    baseQuery.add(mainCriteriaType);
  }



  /**
   * add an order element to the criteria.
   * 
   * @param order
   *          an Order object that specifying the property and the type of order
   *          to perform, use the Orders factory bean for easily specifying the
   *          desired order
   * @return a reference to this criteria for facilitating the query
   *         specification
   */
  public Criteria<MAINTYPE> addOrder(final Order order)
  {
    orders.add(order);
    return this;
  }



  /**
   * Add a field to the results of this query.
   * 
   * @param <TYPE>
   *          the type of the field to add
   * @param type
   *          the class of the type of the field to add
   * @param field
   *          the field to add
   * @return the {@link CriteriaType} to be used to refer to the new field in
   *         the query
   */
  public <TYPE> CriteriaType<TYPE> addProjection(final Class<TYPE> type, final Field field)
  {
    if (projections == null) {
      initProjections();
    }
    final CriteriaType<TYPE> res = newCriteriaType(type);
    projections.put(res, field);
    return res;
  }



  /**
   * Add a field to the results of this query. This method is used only to add a
   * type that has been created with <code>join</code>.
   * 
   * @param criteriaType
   *          the {@link CriteriaType} of the field to add in the query. This
   *          {@link CriteriaType} must already be an existing
   *          {@link CriteriaType} of this criteria.
   */
  public void addProjection(final CriteriaType<?> criteriaType)
  {
    if (projections == null) {
      initProjections();
    }
    projections.put(criteriaType, getPersistenceManager().getFieldManager().getObjectField(criteriaType));
  }



  /**
   * add a restriction criterion to the criteria object. The given restriction
   * describes the query to perform.
   * 
   * @param criterion
   *          a criterion to constrain the results to retrieve, use the
   *          Restrictions factory bean for easily specifying criterion
   * @return a reference to this criteria for facilitating the query
   *         specification
   */
  public Criteria<MAINTYPE> addRestriction(final Criterion criterion)
  {
    if (restrictions == null) {
      restrictions = persistenceManager.getRestrictionsManager().conjunction();
    }
    restrictions.add(criterion);
    return this;
  }



  /**
   * return the main type of this criteria. The main type is the type that this
   * criteria was created with.
   * 
   * @return the main type of this criteria
   */
  public CriteriaType<MAINTYPE> getMainType()
  {
    return mainCriteriaType;
  }



  /**
   * getter for the originalCriteriaType property.
   * 
   * @return the originalCriteriaType
   */
  public CriteriaType<?> getOriginalCriteriaType()
  {
    return originalCriteriaType;
  }



  /**
   * add a grouping clause.
   * 
   * @param field
   *          the field to group by
   */
  public void groupBy(final Field field)
  {
    groupBys.add(field);
  }



  /**
   * returns if this {@link Criteria} is an aggregate.
   * 
   * @return <code>true</code> if this {@link Criteria} is an aggregate,
   *         <code>false</code> otherwise.
   * @see com.genia.toolbox.persistence.criteria.Criteria#isAggregate()
   */
  public boolean isAggregate()
  {
    if (!groupBys.isEmpty()) {
      return true;
    }
    if (projections != null) {
      for (final Field field : projections.values()) {
        if (field.isAggregate()) {
          return true;
        }
      }
    }
    return false;
  }



  /**
   * Perfrom an inner join of this criteria with the type passed in parameter.
   * 
   * @param <TYPE>
   *          the type of the object with which we do a join
   * @param type
   *          the class of the object with which we do a join
   * @return the {@link CriteriaType} to be used to refer to the new object in
   *         the join
   */
  public <TYPE> CriteriaType<TYPE> join(final Class<TYPE> type)
  {
    final CriteriaType<TYPE> res = newCriteriaType(type);
    baseQuery.add(res);
    return res;
  }



  /**
   * getter for the baseQuery property.
   * 
   * @return the baseQuery
   */
  protected List<? extends CriteriaType<?>> getBaseQuery()
  {
    return baseQuery;
  }



  /**
   * getter for the groupBys property.
   * 
   * @return the groupBys
   */
  protected List<? extends Field> getGroupBys()
  {
    return groupBys;
  }



  /**
   * getter for the orders property.
   * 
   * @return the orders
   */
  protected List<? extends Order> getOrders()
  {
    return orders;
  }



  /**
   * getter for the persistenceManager.
   * 
   * @return the persistenceManager
   */
  public PersistenceManager getPersistenceManager()
  {
    return persistenceManager;
  }



  /**
   * getter for the projections property.
   * 
   * @return the projections
   */
  protected Map<? extends CriteriaType<?>, ? extends Field> getProjections()
  {
    return projections;
  }



  /**
   * getter for the innerJoins property.
   * 
   * @return the innerJoins
   */
  protected Map<? extends CriteriaType<?>, ? extends Pair<? extends CriteriaType<?>, PropertyField<?, ?>>> getInnerJoins()
  {
    return innerJoins;
  }



  /**
   * getter for the restrictions property.
   * 
   * @return the restrictions
   */
  protected Junction getRestrictions()
  {
    return restrictions;
  }



  /**
   * returns a new {@link CriteriaType} associated to the type given in
   * paramters.
   * 
   * @param <TYPE>
   *          the type of the {@link CriteriaType}
   * @param type
   *          the class of the type of the {@link CriteriaType}
   * @return a new {@link CriteriaType} associated to the type given in
   *         paramters
   */
  protected abstract <TYPE> CriteriaType<TYPE> newCriteriaType(Class<TYPE> type);



  /**
   * Init the projections of this criteria with all the base types.
   */
  private void initProjections()
  {
    projections = new HashMap<CriteriaType<?>, Field>();
    for (final CriteriaType<?> criteriaType : baseQuery) {
      addProjection(criteriaType);
    }
  }



  /**
   * getter for the distinct property.
   * 
   * @return the distinct
   */
  public boolean isDistinct()
  {
    return distinct;
  }



  /**
   * setter for the distinct property.
   * 
   * @param distinct
   *          the distinct to set
   */
  public void setDistinct(boolean distinct)
  {
    this.distinct = distinct;
  }

}
